package com.frank.demo.io.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

public class NioService {
    private void onCreate(Selector selector, ServerSocketChannel server) throws IOException {
        //服务器端口绑定
        server.socket().bind(new InetSocketAddress(7777), 1024);
        // 设置为非阻塞模式, 这个非常重要
        server.configureBlocking(false);
        // 在选择器里面注册关注这个服务器套接字通道的accept事件
        // ServerSocketChannel只有OP_ACCEPT可用，OP_CONNECT,OP_READ,OP_WRITE用于SocketChannel
        //注册方法返回 SelectionKey 对象  http://ifeve.com/selectors/
        SelectionKey selectionKey = server.register(selector, SelectionKey.OP_ACCEPT);// SelectionKey.OP_READ | SelectionKey.OP_WRITE 这样同时监听读写
    }

    public void init() throws IOException {
        Charset charset = Charset.forName("UTF-8");
        // 创建一个选择器，可用close()关闭，isOpen()表示是否处于打开状态，他不隶属于当前线程
        Selector selector = Selector.open();
        // 创建ServerSocketChannel
        ServerSocketChannel server = ServerSocketChannel.open();

        //初始化配置
        onCreate(selector, server);


        while (true) {
            //多路复用器开始工作（轮询），选择已就绪的通道
            //等待某个通道准备就绪时最多阻塞1秒，若超时则返回。
            selector.select(1000);

            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> it = keys.iterator();

            while (it.hasNext()) {
                //如果key对应的Channel包含客户端的链接请求
                // OP_ACCEPT 这个只有ServerSocketChannel才有可能触发
                SelectionKey key = it.next();
                // 由于select操作只管对selectedKeys进行添加，所以key处理后我们需要从里面把key去掉
                it.remove();
                handleInput(selector, key);
            }
        }
    }

    private void handleInput(Selector selector, SelectionKey key) throws IOException {
        if (key.isAcceptable()) {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            // 得到与客户端的套接字通道
            //ServerSocketChannel的accept接收客户端的连接请求并创建SocketChannel实例，完成上述操作后，相当于完成了TCP的三次握手，TCP物理链路正式建立。
            //我们需要将新创建的SocketChannel设置为异步非阻塞，同时也可以对其TCP参数进行设置，例如TCP接收和发送缓冲区的大小等。此处省掉
            SocketChannel channel = ssc.accept();
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_READ);
            //将key对应Channel设置为准备接受其他请求
            key.interestOps(SelectionKey.OP_ACCEPT);
        }
        if (key.isReadable()) {
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            try {
                int readBytes = channel.read(byteBuffer);
                if (readBytes > 0) {
                    String info = new String(byteBuffer.array());
                    System.out.println(info);
                    if (info.contains("x")) key.channel().close();
                    byteBuffer.flip(); //为write()准备
                    //回应客户端
                    doWrite(channel);
                }
                // 写完就把状态关注去掉，否则会一直触发写事件(改变自身关注事件)
                key.interestOps(SelectionKey.OP_READ);
            } catch (IOException i) {
                //如果捕获到该SelectionKey对应的Channel时出现了异常,即表明该Channel对于的Client出现了问题
                //所以从Selector中取消该SelectionKey的注册
                key.cancel();
                if (key.channel() != null) {
                    key.channel().close();
                }
            }
        }
    }

    private void doWrite(SocketChannel sc) throws IOException {
        byte[] req = "hasreceive".getBytes();
        ByteBuffer byteBuffer = ByteBuffer.allocate(req.length);
        byteBuffer.put(req);
        byteBuffer.flip();
        sc.write(byteBuffer);
        if (!byteBuffer.hasRemaining()) {
            System.out.println("Send to Service successed");
        }
    }

    public static void main(String[] args) throws IOException {
        new NioService().init();
    }
}